@page "/todo"
@inject TodoService TodoService
@implements IDisposable

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

@foreach (var todo in todos)
{
    <div class="input-group mb-3">
        <div class="input-group-prepend">
            <div class="input-group-text">
                <input type="checkbox" checked="@todo.IsDone" @onchange="@(async e => await HandleTodoDoneAsync(todo, (bool)e.Value))" />
            </div>
            <button class="btn btn-outline-secondary" type="button" @onclick="@(async e => await HandleDeleteTodoAsync(todo))">
                <span class="oi oi-trash" aria-hidden="true"></span>
            </button>
        </div>
        <input class="form-control" value="@todo.Title" @onchange="@(async e => await HandleTodoTitleChangeAsync(todo, (string)e.Value))" />
    </div>
}

<input placeholder="Something todo" @bind="@newTodo" />
<button @onclick="AddTodoAsync">Add Todo</button>

@code {

    private Guid ownerKey = Guid.Empty;
    private TodoKeyedCollection todos = new TodoKeyedCollection();
    private string newTodo;
    private StreamSubscriptionHandle<TodoNotification> subscription;

    protected override async Task OnInitializedAsync()
    {
        // subscribe to updates for the current list
        // note that the blazor task scheduler is reentrant
        // therefore notifications can and will come through when the code is stuck at an await
        subscription = await TodoService.SubscribeAsync(ownerKey, notification => InvokeAsync(() => HandleNotificationAsync(notification)));

        // get all items from the cluster
        foreach (var item in await TodoService.GetAllAsync(ownerKey))
        {
            todos.Add(item);
        }

        await base.OnInitializedAsync();
    }

    public void Dispose()
    {
        // unsubscribe from the orleans stream - best effort
        try
        {
            subscription?.UnsubscribeAsync();
        }
        catch
        {
            // noop
        }
    }

    private async Task AddTodoAsync()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            // create a new todo
            var todo = new TodoItem(Guid.NewGuid(), newTodo, false, ownerKey);

            // add it to the cluste
            await TodoService.SetAsync(todo);

            // the above this will generate a stream notification that may or may not have come through while we were awaiting the call
            // therefore only add it to the interface if it is not there yet
            if (todos.TryGetValue(todo.Key, out var current))
            {
                // latest one wins
                if (todo.Timestamp > current.Timestamp)
                {
                    todos[todos.IndexOf(current)] = todo;
                }
            }
            else
            {
                todos.Add(todo);
            }

            // reset the text box
            newTodo = null;
        }
    }

    private Task HandleNotificationAsync(TodoNotification notification)
    {
        // was the item removed
        if (notification.Item == null)
        {
            // attempt to remove it from the user interface
            if (todos.Remove(notification.ItemKey))
            {
                StateHasChanged();
            }
            return Task.CompletedTask;
        }

        if (todos.TryGetValue(notification.Item.Key, out var current))
        {
            // latest one wins
            if (notification.Item.Timestamp > current.Timestamp)
            {
                todos[todos.IndexOf(current)] = notification.Item;
                StateHasChanged();
            }
            return Task.CompletedTask;
        }

        todos.Add(notification.Item);
        StateHasChanged();
        return Task.CompletedTask;
    }

    private void TryUpdateCollection(TodoItem item)
    {
        // we need to cater for reentrancy allowing a stream notification during the previous await
        // the notification may have even have deleted the item - if so then deletion wins
        if (todos.TryGetValue(item.Key, out var current))
        {
            // latest one wins
            if (item.Timestamp > current.Timestamp)
            {
                todos[todos.IndexOf(current)] = item;
            }
        }
    }

    private async Task HandleTodoDoneAsync(TodoItem item, bool isDone)
    {
        var updated = item.WithIsDone(isDone);
        await TodoService.SetAsync(updated);
        TryUpdateCollection(updated);
    }

    private async Task HandleTodoTitleChangeAsync(TodoItem item, string title)
    {
        var updated = item.WithTitle(title);
        await TodoService.SetAsync(updated);
        TryUpdateCollection(updated);
    }

    private async Task HandleDeleteTodoAsync(TodoItem item)
    {
        await TodoService.DeleteAsync(item.Key);
        todos.Remove(item.Key);
    }

    private class TodoKeyedCollection : KeyedCollection<Guid, TodoItem>
    {
        protected override Guid GetKeyForItem(TodoItem item) => item.Key;
    }
}